#!/usr/bin/env php
<?php
/**
 * socket message to xmpp muc daemon
 *
 * @version     $Id: mucbotd.php 13 2008-02-15 21:27:15Z CXJohnson $
 * @author      Chris Johnson (tinpixel.com)
 * @copyright   2007, Chris Johnson
 * @see         License: GPLv3
 * @package
 */

declare(ticks = 1);                     // weird PHP signal handling requirement

require 'class.jabber.php';

$prognam = basename($argv[0]);
$xmpp = NULL;                           // refactor
$cfg = NULL;                            // refactor
set_exception_handler('handle_exception');
error_reporting(E_ALL & ~E_NOTICE);     // class.jabber.php is full of undeclared variable usage
main($prognam);


/*
 * TODO:
 1. check that all cfg values are set or defaulted or error messaged.
 2. trap signals where possible and... DONE
 3. log events, signals, exits, etc.    MOSTLY DONE
 *
 */

function main($prognam) {
    global $xmpp;                   // todo
    global $cfg;                    // todo
    global $debug;                  // todo

    $shortoptions = 'f:d';
    $args = getopt($shortoptions);

    /* get required configuration file info */
    $cfgfile = 'mucbotd.ini';
    if (isset($args['f'])) {
        $cfgfile = $args['f'];
    }
    $cfg = @parse_ini_file($cfgfile);
    if (empty($cfg)) {
        mydie('need a config file to run');
    }
    $botalias = ($cfg['botalias']) ? $cfg['botalias'] : 'muc.bot';

    if (isset($args['d'])) {
        $debug = TRUE;
    }

    setsighandlers();           /* install signal handlers */

    setunilock($prognam) or mydie('another copy already running or bad lock remains');

    /* initialize TCP listening socket */
    $sock = startsocket($cfg['msgserver'], $cfg['msgport'], $cfg['msgmaxcli']);
    if ($sock == NULL) {                // refactor
        mydie('cannot create message socket');
    }

    /* become a daemon */
    if (!$debug) {
        daemonize($cfg) or mydie('unable to daemonize');
    }

    /* initalize XMPP MUC connection */
    $xmpp = joinmuc($cfg['mucuser'], $cfg['mucpass'], $cfg['mucid'], $botalias, $cfg['mucserver'], $cfg['mucport']);
    if ($xmpp == NULL) {                // refactor
        mydie('cannot create xmpp connection');
    }

    /* LISTEN for messages on socket */
    socket_listen($sock);

    send($xmpp, $cfg['mucid'], "Hello.  Now taking messages on port {$cfg['msgport']}.");           // refactor

    $run = TRUE;
    while ($run) {
        sleep(1);           // avoid tight cpu loop.  msgs are async, no hurry.
        $xmpp->SendPresence(NULL, $cfg['mucid'] .'/'. $botalias, 'online');
        /* handle any messages received on the listening socket */
        $msg = trim(getmsg($sock));
        if ($msg == NULL) {
            continue;       // no messages, try again.
        }
        /* do any received-message parsing/formatting here */
        if ($msg == 'haltrun') {
            $run = FALSE;
            continue;
        }
        /* send the received message to the MUC */
        send($xmpp, $cfg['mucid'], $msg);        // refactor

        /* check for xmpp callbacks & messages, wait no more than 1 second */
        $xmpp->CruiseControl(1);

        /* handle any XMPP messages received here, if so desired */
    }
    /* $run is now false. */
    finishup('Bot has been told to halt.  Goodbye.');   
    exit;   // insurance
}
/* ----------------------------------------------------------------------- */

/**
 * signal handler
 */
function sighandler($signo) {
    global $xmpp, $cfg;             // refactor
    switch ($signo) {
    case SIGHUP:
    case SIGQUIT:
        mylog("caught signal '$signo'\n");
        finishup('Bot has been told to halt.  Goodbye.');   
        break;
    case SIGINT:
    case SIGTERM:
        mylog("caught SIGTERM, exiting...\n");
        finishup('Bot has been told to halt.  Goodbye.');   
        break;
    case SIGCHLD:
    case SIGUSR1:
    case SIGUSR2:
        mylog("caught signal '$signo'\n");
        break;
    default:
        mylog("caught signal '$signo'\n");
    }
    exit;
}

/**
 * set signal handlers
 */
function setsighandlers() {
    pcntl_signal(SIGHUP,  "sighandler");   // 1, terminal line hangup (term)
    pcntl_signal(SIGINT,  "sighandler");   // 2, interrupt prog (term) (ctl-c)
    pcntl_signal(SIGQUIT, "sighandler");   // 3, quit program (core)
    pcntl_signal(SIGTERM, "sighandler");   // 15, terminate process (term)
    pcntl_signal(SIGCHLD, "sighandler");   // 20, child status changed (ign)
    pcntl_signal(SIGUSR1, "sighandler");   // 30, user defined signal 1 (term)
    pcntl_signal(SIGUSR2, "sighandler");   // 31, user defined signal 2 (term)
}

/**
 * Issue a message to the system log.
 */
function mylog($msg) {
    global $prognam;            // todo
    static $firstcall = TRUE;
    if ($firstcall) {
        openlog($prognam, LOG_ODELAY, LOG_USER);
        $firstcall = FALSE;
    }
    syslog(LOG_INFO, $msg);
}

/**
 * abort and exit with a message.
 *
 * @param msg
 *  The message to issue.
 */
function mydie($msg) {
    // refactor
    global $prognam;

    clearunilock($prognam);
    mylog("$prognam: $msg");
    die("$prognam: $msg\n");
}

/**
 * default exception handler.
 */
function handle_exception($e) {
    echo "exception caught:\n".
            "file ". $e->getFile() ." at line ". $e->getLine() ."\n".
            $e->getMessage() ."(". $e->getCode() .")\n";
    finishup('exception caught');       // no return
}

/**
 * notify intention to exit and clean up.
 *
 * @param msg       The message to issue, if any.
 */
function finishup($msg = 'exiting') {
    global $prognam;                        // refactor
    global $xmpp, $cfg;                     // refactor
    send($xmpp, $cfg['mucid'], $msg);       // refactor
    $xmpp->disconnect();
    clearunilock($prognam);
    mylog($msg);
    exit;
}

/**
 * join the xmpp muc
 *
 * @param user          xmpp username string
 * @param pass          xmpp username password string
 * @param mucid         xmpp muc identifier e.g. muc@conference.example.com
 * @param alias         user alias on muc
 * @param server        xmpp server name string
 * @param port          xmpp server port number as a string
 * @returns             jabber class object handle or NULL if failure
 */
function joinmuc($user, $pass, $mucid, $alias = 'muc', $server = 'localhost', $port = '5222') {
    global $debug;

    mylog("joining MUC $mucid");
    $jab = new Jabber;
    $jab->username = $user;
    $jab->password = $pass;
    $jab->server = $server;
    $jab->port   = $port;

    /* DEBUG */
    // todo
    $jab->log_filename = 'mucbotd.debug.log';
    $jab->log_filehandler = TRUE;
    $jab->enable_logging = $debug;
    /* END DEBUG */

    if (!$jab->Connect()) return NULL;
    if (!$jab->SendAuth()) return NULL;
    if (!$jab->SubscriptionAcceptRequest($mucid)) return NULL;
    if (!$jab->SendPresence(NULL, $mucid .'/'. $alias, 'online')) return NULL;
    return $jab;
}

/**
 * start the message listening socket service
 */
function startsocket($host = 'localhost', $port = 9000, $maxclients = 10) {
    mylog('starting socket');
    $sock = socket_create(AF_INET, SOCK_STREAM, 0); 
    if ($sock === FALSE) {
        return NULL;
    }
    $address = gethostbyname(trim($host));
    if (socket_bind($sock, $address, $port) === FALSE) {
        return NULL;
    }
    if (socket_listen($sock) === FALSE) {
        return NULL;
    }
    mylog("listening at host $address, port $port\n");
    return $sock;
}

/**
 * get next message from client on socket, if any.
 * This function is the most in need of refactoring.
 */
function getmsg($sock) {
    global $debug;                  // todo
    static $clients = array();      // socket array of clients with data to read
    $read = array();
    $write = NULL;
    $except = NULL;

    if (count($clients) == 0) {
        $clients = array($sock);
    }

    $read = $clients;
    if (socket_select($read, $write, $except, 2) < 1) {
        // no sockets ready to read, and thus no clients.
        return NULL;
    }
    // look for client trying to connect for first time
    if (in_array($sock, $read)) {
        // accept the connection
        $newsock = socket_accept($sock);
        $clients[] = $newsock;      // remember

        // send client a welcome message
        socket_write($newsock, 'Hello.  There are '. (count($clients)-1). " client(s) connected to the server\n");
        socket_getpeername($newsock, $ip);
        mylog("New client connected: {$ip}\n");
        if ($debug) {
            echo "New client connected: {$ip}\n";
        }

        // remove the listening socket from the clients-with-data array
        $key = array_search($sock, $read);
        unset($read[$key]);
    }

    // read data from the next available client
    foreach ($read as $read_sock) {
        // read until newline or 1024 bytes  --fixme--

        if ($debug) {
            echo "trying to read '$read_sock'\n";
        }

        $data = @socket_read($read_sock, 1024, PHP_NORMAL_READ);

        // check if client disconnected
        if ($data === FALSE) {
            // remove client from list of ready to read clients
            $key = array_search($read_sock, $clients);
            unset($clients[$key]);
            mylog("client $key disconnected\n");
            continue;
        }
        break;
    }
    // got a message
    mylog("Message from client ". array_search($read_sock, $clients) ."\n");
    @fclose($read_sock);
    if ($debug) {
        echo "got a message...\n";      /* DEBUG */
    }
    $data = trim($data);
    if (!empty($data)) {
        return $data;
    } else {
        return NULL;
    }
}
/**
 * detach from initiating process and become a daemon
 */
function daemonize($cfg) {
    mylog('parent:  daemonizing');
    if (pcntl_fork()) {
        // parent
        exit;
    }
    // reset session id to start new process group leader
    posix_setsid();
    if (pcntl_fork()) {
        // parent -- magical PHP incantation :-p
        exit;
    }
    mylog('child:  now a daemon, giving up privs');
    // chroot and give up privs if superuser
    if (posix_getuid() == 0 || posix_geteuid() == 0) {
        chroot($cfg['chroot']);
        $pw = posix_getpwnam('nobody');
        posix_setuid($pw['uid']);
        posix_setgid($pw['gid']);
    } else {
        chdir($cfg['chdir']);
    }
    return TRUE;
}

/**
 * set exclusivity lock
 */
function setunilock($prog) {
    $lockfile = "/tmp/$prog.lockfile";
    $fp = fopen($lockfile, 'a');
    if (!$fp || !flock($fp, LOCK_EX | LOCK_NB)) {
        fputs(STDERR, "$prog: failed to acquire lock.  already running?\n");
        readfile($lockfile);
        return FALSE;
    }
    @fwrite($fp, date('r'. "\n"));
    return TRUE;
}

/**
 * remove exclusivity lock on exit
 */
function clearunilock($prog) {
    @unlink("/tmp/$prog.lockfile");
}

/**
 * send a message to muc using class.jabber.php API
 */
function send($xmpp, $id, $msg) {            // refactor
    $xmpp->SendMessage($id, 'groupchat', NULL, array('body' => htmlspecialchars($msg)), $payload = NULL);
}

/**
 * override muc message handler.
 */
function XHandler_message_groupchat($packet) {
    recv($packet);
}
function XHandler_message_chat($packet) {
    recv($packet);
}
function recv($packet) {
    global $xmpp;
    $from = $xmpp->GetInfoFromMessageFrom($packet);
    $body = $xmpp->GetInfoFromMessageBody($packet);
    /* DEBUG */
    echo "from: $from - body: $body\n";
    /* END DEBUG */
}
?>
